/*
 * Copyright (c) 2008, Takashi Ito
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the authors nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "tansu_internal.h"
#include "tansu_status.h"
#include "tansu_nwif.h"
#include "tansu_util.h"

struct tansu_nwif_monitor *
tansu_nwif_monitor_new()
{
    struct tansu_nwif_monitor *monitor =
        (struct tansu_nwif_monitor *)
        malloc(sizeof(struct tansu_nwif_monitor));

    if (monitor) {
        memset(monitor, 0, sizeof(struct tansu_nwif_monitor));
        STAILQ_INIT(&monitor->listeners);
    } else {
        TLOG_ERROR(TLOG_NWIF, "malloc() failed.\n");
    }

    return monitor;
}

void
tansu_nwif_monitor_delete(struct tansu_nwif_monitor *monitor)
{
    struct tansu_nwif_listener *listener;

    if (monitor == NULL)
        return;

    tansu_nwif_freeaddrs(monitor->latest);
    monitor->latest = NULL;

    while (!STAILQ_EMPTY(&monitor->listeners)) {
        listener = STAILQ_FIRST(&monitor->listeners);
        STAILQ_REMOVE_HEAD(&monitor->listeners, entry);
        free(listener);
    }

    free(monitor);
    monitor = NULL;
}

struct tansu_nwif_listener *
tansu_nwif_monitor_add(struct tansu_nwif_monitor *monitor,
                       TANSU_NWIF_EVENT_FUNC(callback),
                       void *userdata)
{
    struct tansu_nwif_listener *listener;

    if (!(monitor && callback)) {
        TLOG_WARN(TLOG_NWIF, "Invalid argument\n");
        return NULL;
    }
    
    listener = (struct tansu_nwif_listener *)
        malloc(sizeof(struct tansu_nwif_listener));

    if (listener == NULL) {
        TLOG_ERROR(TLOG_NWIF, "malloc() failed.\n");
        return NULL;
    }

    STAILQ_NEXT(listener, entry) = NULL;
    listener->callback = callback;
    listener->userdata = userdata;

    STAILQ_INSERT_TAIL(&monitor->listeners, listener, entry);

    return listener;
}

void
tansu_nwif_monitor_remove(struct tansu_nwif_monitor *monitor,
                          struct tansu_nwif_listener *listener)
{
    if (monitor && listener)
        STAILQ_REMOVE(&monitor->listeners, listener, tansu_nwif_listener, entry);

    free(listener);
}

uint32_t
tansu_nwif_compare(struct tansu_nwif *current,
                   struct tansu_nwif *old)
{
    uint32_t event = 0;

    if (!(current && old)) {
        TLOG_WARN(TLOG_NWIF, "Invalid argument\n");
        return 0;
    }

    if (strcmp(current->addr, old->addr))
        event |= TANSU_NWIF_EVENT_ADDR_CHANGED;

    if (strcmp(current->netmask, old->netmask))
        event |= TANSU_NWIF_EVENT_ADDR_CHANGED;

    if (current->flags != old->flags)
        event |= TANSU_NWIF_EVENT_FLAG_CHANGED;

    return event;
}

static int
append_events(struct tansu_nwif_events *events,
              struct tansu_nwif *nwif,
              uint32_t flag)
{
    int n = 0;
    struct tansu_nwif_events new_events = STAILQ_HEAD_INITIALIZER(new_events);

    while (nwif) {
        struct tansu_nwif_event *event;
        event = (struct tansu_nwif_event *)
            malloc(sizeof(struct tansu_nwif_event));
        if (event == NULL) {
            TLOG_ERROR(TLOG_NWIF, "malloc failed.\n");
            n = -1;
            break;
        }
        event->event = flag;
        if (flag & TANSU_NWIF_EVENT_IF_ADDED) {
            event->before = NULL;
            event->after = nwif;
        } else {
            event->before = nwif;
            event->after = NULL;
        }
        STAILQ_INSERT_TAIL(&new_events, event, entry);
        n++;
        nwif = STAILQ_NEXT(nwif, entry);
    }

    if (n >= 0) {
        struct tansu_nwif_event *event;
        STAILQ_MOVE(event, events, &new_events, entry);
    } else {
        struct tansu_nwif_event *event;
        STAILQ_FREE_ALL(event, &new_events, entry, free);
    }

    return n;
}

static int
add_new_event(struct tansu_nwif_events *events,
              struct tansu_nwif *before,
              struct tansu_nwif *after,
              uint32_t flag)
{
    struct tansu_nwif_event *event;
    event = (struct tansu_nwif_event *)
        malloc(sizeof(struct tansu_nwif_event));
    if (event == NULL) {
        TLOG_ERROR(TLOG_NWIF, "malloc failed.\n");
        return -1;
    }
    event->event = flag;
    event->before = before;
    event->after = after;
    STAILQ_INSERT_TAIL(events, event, entry);
    return 0;
}

static uint32_t
check_differences(struct tansu_nwif *nwif0,
                  struct tansu_nwif *nwif1)
{
    uint32_t flag = 0;
    if (strcmp(nwif0->addr, nwif1->addr) != 0 ||
        strcmp(nwif0->netmask, nwif1->netmask) != 0)
    {
        flag |= TANSU_NWIF_EVENT_ADDR_CHANGED;
    }
    if (nwif0->flags != nwif1->flags)
        flag |= TANSU_NWIF_EVENT_FLAG_CHANGED;

    return flag;
}

/**
 * @retval 0  there is no difference
 * @retval >0 the number of events added
 * @retval -1 memory error
 */
static int
check_nwifs(struct tansu_nwif_events *events,
            struct tansu_nwifs *before,
            struct tansu_nwifs *after)
{
    int ret;
    int diff = 0;
    uint32_t flag;
    struct tansu_nwif_event *event;
    struct tansu_nwif *nwif0 = STAILQ_FIRST(before);
    struct tansu_nwif *nwif1 = STAILQ_FIRST(after);
    struct tansu_nwif_events new_events = STAILQ_HEAD_INITIALIZER(new_events);

    while (1) {
        if (nwif0 == NULL || nwif1 == NULL) {
            struct tansu_nwif *nwif;
            if (nwif0 == NULL) {
                nwif = nwif1;
                flag = TANSU_NWIF_EVENT_IF_ADDED;
            } else {
                nwif = nwif0;
                flag = TANSU_NWIF_EVENT_IF_REMOVED;
            }
            ret = append_events(&new_events, nwif, flag);
            if (ret >= 0) {
                STAILQ_MOVE(event, events, &new_events, entry);
                return diff + ret;
            } else {
                STAILQ_FREE_ALL(event, &new_events, entry, free);
                return ret;
            }
        }

        if (strcmp(nwif0->name, nwif1->name) != 0)
            break;

        if (nwif0->family != nwif1->family)
            break;

        flag = check_differences(nwif0, nwif1);
        if (flag) {
            ret = add_new_event(&new_events, nwif0, nwif1, flag);
            if (ret < 0) {
                STAILQ_FREE_ALL(event, &new_events, entry, free);
                return ret;
            }
            diff++;
        }
        nwif0 = STAILQ_NEXT(nwif0, entry);
        nwif1 = STAILQ_NEXT(nwif1, entry);
    }

    struct tansu_nwif *nwif0_start = nwif0;
    while (nwif0) {
        int matched = 0;
        struct tansu_nwif *tmp = nwif1;
        for (; tmp != NULL; tmp = STAILQ_NEXT(tmp, entry)) {
            if (strcmp(nwif0->name, tmp->name) != 0)
                continue;
            if (nwif0->family != tmp->family)
                continue;
            flag = check_differences(nwif0, tmp);
            if (flag) {
                ret = add_new_event(&new_events, nwif0, tmp, flag);
                if (ret < 0) {
                    STAILQ_FREE_ALL(event, &new_events, entry, free);
                    return ret;
                }
                diff++;
            }
            matched = 1;
            break;
        }
        if (!matched) {
            flag = TANSU_NWIF_EVENT_IF_REMOVED;
            ret = add_new_event(&new_events, nwif0, NULL, flag);
            if (ret < 0) {
                STAILQ_FREE_ALL(event, &new_events, entry, free);
                return ret;
            }
            diff++;
        }
        nwif0 = STAILQ_NEXT(nwif0, entry);
    }

    /* check if new nwifs exist */
    while (nwif1) {
        int matched = 0;
        struct tansu_nwif *tmp = nwif0_start;
        for (; tmp != NULL; tmp = STAILQ_NEXT(tmp, entry)) {
            if (strcmp(nwif1->name, tmp->name) != 0)
                continue;
            if (nwif1->family != tmp->family)
                continue;
            matched = 1;
            break;
        }
        if (!matched) {
            flag = TANSU_NWIF_EVENT_IF_ADDED;
            ret = add_new_event(&new_events, NULL, nwif1, flag);
            if (ret < 0) {
                STAILQ_FREE_ALL(event, &new_events, entry, free);
                return ret;
            }
            diff++;
        }
        nwif1 = STAILQ_NEXT(nwif1, entry);
    }

    STAILQ_MOVE(event, events, &new_events, entry);
    return diff;
}


void
tansu_nwif_monitor_poll(struct tansu_nwif_monitor *monitor)
{
    int ret;
    struct tansu_nwif_event *event;
    struct tansu_nwif_events events = STAILQ_HEAD_INITIALIZER(events);
    tansu_status_t status;
    struct tansu_nwifs *head = tansu_nwif_getaddrs(&status);

    if (head == NULL) {
        TLOG_ERROR(TLOG_NWIF, "tansu_nwif_getaddrs() failed %d\n", status);
        return;
    }

    if (monitor->latest) {
        ret = check_nwifs(&events, monitor->latest, head);
    } else {
        uint32_t flag = TANSU_NWIF_EVENT_IF_ADDED;
        ret = append_events(&events, STAILQ_FIRST(head), flag);
    }

    if (ret == 0) {
        TLOG_INFO(TLOG_NWIF, "no nwif change found\n");
    } else if (ret > 0) {
        struct tansu_nwif_listener *listener;
        TLOG_INFO(TLOG_NWIF, "updated %d nwifs\n", ret);
        STAILQ_FOREACH(listener, &monitor->listeners, entry) {
            listener->callback(&events, listener->userdata);
        }
    } else {
        TLOG_ERROR(TLOG_NWIF, "check_nwifs() failed %d\n", ret);
    }

    STAILQ_FREE_ALL(event, &events, entry, free);
    tansu_nwif_freeaddrs(monitor->latest);
    monitor->latest = head;
}

